package com.espirit.eap.manager.workflow;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;

import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Level;
import org.apache.log4j.Logger;
import org.mvel2.MVEL;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.espirit.common.Message;
import com.espirit.common.WFMessage;
import com.espirit.common.WFMessageService;
import com.espirit.eap.manager.I18n;
import com.espirit.eap.manager.I18nService;
import com.espirit.eap.manager.WFStatusManager;
import com.espirit.eap.manager.processor.BizNodeInfo;
import com.espirit.eap.pagelayout.ModelService;
import com.espirit.eap.sa.LoginUser;
import com.espirit.eap.sa.LoginUserService;
import com.espirit.eap.util.Functions;
import com.googlecode.cswish.exception.ApplicationException;
import com.googlecode.cswish.model.MapContext;
import com.googlecode.cswish.oosearch.QLInfo;
import com.googlecode.cswish.struts.ContextHolder;
import com.googlecode.cswish.struts.Invoker;
import com.googlecode.cswish.struts.Routing;
import com.googlecode.cswish.struts.hibernate.HibernateUtil;
import com.googlecode.cswish.struts.spring.BeanIntrospector;
import com.googlecode.cswish.struts.spring.CacheService;
import com.googlecode.cswish.struts.spring.CacheService.Wrapper;
import com.googlecode.cswish.struts.spring.Executor;
import com.googlecode.cswish.struts.spring.GenericService;
import com.googlecode.cswish.struts.spring.ObjectFactory;
import com.googlecode.cswish.util.ClassUtil;
import com.googlecode.cswish.util.FreemarkerTplEngine;
import com.googlecode.cswish.util.InvocationUtil;
import com.googlecode.cswish.util.InvocationUtil.ServiceInfo;

@Service
public class WFInstanceService {

	private final static Logger logger = Logger.getLogger(WFInstanceService.class);
	private static final String WF_USEREX = "userEx";
	
	@Resource
	private GenericService genericService;
	
	@Resource
	private LoginUserService loginUserService;
	
	@Resource
	private WFStatusManager wfStatusManager;
	
	@Resource
	private I18nService i18nService;
	
	@Resource
	private ModelService modelService;
	
	@Resource
	private BeanIntrospector beanIntrospector;
	
	@Resource
	private ObjectFactory objectFactory;
	
	@Resource
	private Routing routing;
	
	@Resource
	private Functions functions;
	
	@Resource
	private InvocationUtil invocationUtil;
	
	@Resource
	private WFMessageService wfMessageService;
	
	@Resource
	private CacheService cacheService;
	
	private Map<String, List<NodeExecutorInfo>> autoNodeCache;
	
	private Wrapper<Executor<Object, Object[]>> notice;
	
	class NodeExecutorInfo {
		String condition;
		ServiceInfo serviceInfo;
		
		public NodeExecutorInfo(ServiceInfo serviceInfo, String condition) {
			this.serviceInfo = serviceInfo;
			this.condition = condition;
		}
	}
	
	@PostConstruct
	private void init() {
		Wrapper noticeVar = cacheService.newVar(Executor.class);
		this.notice = noticeVar;
	}
	
	public void registerNotice(Executor<Object, Object[]> callback) {
		notice.set(callback);
	}
	
	@Transactional
	public void start(WFInstance model, Map action) {
		WorkflowAware workflowAware = getSourceEntity(model);
		workflowAware = genericService.load(workflowAware, false);
		
		WFTemplate wftemplate = model.getWfTemplate();
		wftemplate = genericService.load(wftemplate, false);
		
		if (!needTemplate(wftemplate, workflowAware, action)){
			accept(workflowAware);
			return ;
		}
		
		WFPhase phase = getTemplateFirstPhase(wftemplate, workflowAware, action);
		if (phase == null){
			accept(workflowAware);
			return ;
		}
		
		WFInstance result = generateInstance(workflowAware, wftemplate, phase, model.getDescription(), action);
		if (workflowAware.isAccept()) return ;
		if (result != null){
			start(workflowAware);
		}
	}
	
	public WFPhase getTemplateFirstPhase(WFTemplate wftemplate, WorkflowAware event, Map action) {
		return getTemplateNextPhase(wftemplate, event, null, action);
	}
	
	
	public WFPhase getTemplateNextPhase(WFTemplate wftemplate, WorkflowAware event, WFPhase currentPhase, Map action) {
		List<WFPhase> phases = wftemplate.getPhases();
		if (phases != null && phases.size() > 0){
			WFPhase phase = null;
			int size = phases.size();
			if (currentPhase == null){
				phase =  phases.get(0);
			} else {
				for(int i=0 ; i< size ; i++){
					if (phases.get(i).getId().equals(currentPhase.getId())){
						if (i == size -1){ 
							return null;
						} else {
							phase = phases.get(++i);
						}
					}
				}
			}
			boolean valid = isValidPhase(phase, event, action);
			if (valid){
				return phase ;	
			} else {
				return getTemplateNextPhase(wftemplate, event, phase, action);
			}
			
		}
		return null;
	}
	
	public boolean isValidPhase(WFPhase phase, WorkflowAware model, Map action){
		WFCondition wfCondition = phase.getWfCondition();
		if (wfCondition != null){
			return checkCondition(wfCondition, model, action);
		} else {
			return true;
		}
	}
	
	private boolean needTemplate(WFTemplate wftemplate, WorkflowAware workflowAware, Map action) {
		WFCondition wfCondition = wftemplate.getWfCondition();
		if (wfCondition != null){
			return checkCondition(wfCondition, workflowAware, action);
		} else {
			return true;
		}
	}
	
	public boolean checkCondition(WFCondition wfCondition, WorkflowAware model, Map action) {
		if (wfCondition == null) return true;
		Integer acceptTrigger = wfCondition.getAcceptTrigger();
		if (acceptTrigger != null && acceptTrigger.equals(WFCondition.ACCEPT_TRIGGER_ALWAYS)){
			return true;
		} else {
			String triggerCondition = wfCondition.getTriggerCondition();
			if (triggerCondition == null || triggerCondition.isEmpty()){
				return true;
			} else {
				String condtion = triggerCondition.replaceAll(" > ", " gt ");
				condtion = condtion.replaceAll(" > ", " gt ");
				condtion = condtion.replaceAll(" < ", " lt ");
				condtion = condtion.replaceAll(" >= ", " gte ");
				condtion = condtion.replaceAll(" <= ", " lte ");
				condtion = condtion.replaceAll(" and ", " && ");
				condtion = condtion.replaceAll(" or ", " || ");
				
				Map<String, Object> parameters = new HashMap<String, Object>();
				invocationUtil.forceLoadModelAndResults(parameters);
				parameters.put("fn", functions);
				parameters.put("model", model);
				parameters.put("action", new MapContext(action));
				
				Map<String, Object> traceResult = new HashMap<String, Object>(4);
				traceResult.put("result", null);

				FreemarkerTplEngine.getInstance().execute(condtion, parameters, traceResult);
				String result = (String) traceResult.get("result");
				return Boolean.valueOf(result);
				//throw new ApplicationException("errors.workflow.condition.expressionError" );
			}
		}
	}
	
	public WorkflowAware getSourceEntity(WFInstance instance){
		try {
			Class clazz = ClassUtil.getClass(instance.getSrcClassName());
			WorkflowAware result = (WorkflowAware)genericService.load(clazz, instance.getSrcId());
			return result;
		} catch (Exception e) {
			return null;
		}
	}
	
	private WFInstance generateInstance(WorkflowAware workflowAware, WFTemplate wftemplate, WFPhase phase, String desc, Map action){
		WFInstance nextInstance = new WFInstance();
		nextInstance.setWfTemplate(wftemplate);
		nextInstance.setWfPhase(phase);
		nextInstance.setDescription(desc);
		
		nextInstance.onSetSrcClassName(workflowAware);
		nextInstance.setSrcId(workflowAware.getId());
		
		nextInstance = genericService.add(nextInstance);
		if (phase != null ){
			List<LoginUser> phaseUsers = new ArrayList<LoginUser>(phase.getUsers());
			checkCondition(phase.getWfCondition(),  workflowAware, action);
			List<String> userCodes = (List<String>) action.get(WF_USEREX);
			if (userCodes != null && !userCodes.isEmpty()){
				for(String userCode : userCodes){
					Integer userId = loginUserService.getLoginUserIdByCode(userCode);
					LoginUser user = genericService.load(LoginUser.class, userId);
					if (user != null){
						phaseUsers.add(user);
					}
				}
			}
			if (phaseUsers != null && !phaseUsers.isEmpty()){
				List<LoginUser> validUsers = new ArrayList<LoginUser>();
				for(LoginUser user : phaseUsers){
					if (hasEventPrivilege(workflowAware, user)){
						validUsers.add(user);
					}
				}
				if (validUsers.isEmpty() ){
					throw new ApplicationException("errors.workflow.phase.userHasNoPrivilege" ,phase.getName() ,MVEL.getProperty(workflowAware.getSrcEntityKey(), workflowAware));
				} else {
					if (validUsers.size() < phase.safeNeedAcceptUsers()){
						throw new ApplicationException("errors.workflow.phase.usersNotEnough" , phase.getName() ,MVEL.getProperty(workflowAware.getSrcEntityKey(), workflowAware));	
					} else {
						for(LoginUser user : validUsers){
							WFMessage wfmessage = (WFMessage) generateWFMessage(nextInstance, user);
							
							
							WFInstanceTracker instanceTracker = new WFInstanceTracker();
							instanceTracker.setUser(user);
							instanceTracker.setCreatedOn(new Date());
							instanceTracker.setParent(nextInstance);
							instanceTracker = genericService.add(instanceTracker);
							nextInstance.safeInstanceTrackers().add(instanceTracker);
							
							
							//auto process ,when user is me
							if (user.getId().equals(loginUserService.getLoginUser().getId())){
								WFStatus accept = wfStatusManager.getAccept();
								instanceTracker.setStatus(accept);
								genericService.merge(instanceTracker);
								wfmessage.setStatus(accept);
								process(wfmessage, action);
							}
						}
					}
				}
			} else {
				throw new ApplicationException("errors.workflow.phase.hasNoUsers" , phase.getName());
			}
		}
		return nextInstance;
	}
	
	private boolean hasEventPrivilege(WorkflowAware workflowAware ,LoginUser user){
		String ql = "select a  from " + workflowAware.getClass().getName() + " a where a.id =?" ;
		QLInfo qlInfo = new QLInfo(ql ,false ,workflowAware.getId());
		genericService.activePermissionFilter(qlInfo, user.getId().toString());
		return  genericService.searchByQl(qlInfo, false).getData(0) != null;
	}
	
	private WFInstanceTracker getMyInstanceTracker(WFInstance instance){
		LoginUser me = loginUserService.getLoginUser();
		String ql = "select a from " +  WFInstanceTracker.class.getName() + " a where a.parent.id=? and a.user.id=?";
		QLInfo qlInfo = new QLInfo(ql, true, instance.getId(),me.getId());
		WFInstanceTracker tracker = (WFInstanceTracker) genericService.searchByQl(qlInfo, false).getData(0);
		if (tracker == null){
			throw new ApplicationException("Unknow errors ,please contact admin");
		} else {
			return tracker;
		}
	}
	
	@Transactional
	public void process(WFMessage model, Map action){
		WFStatus decide = wfStatusManager.getWFStatus(model.getStatus());
		String approveRemark = model.getDescription();
		
		if (decide == null){
			throw new ApplicationException("errors.workflow.accept.statusIsNull");
		}
		WFMessage wfMessage = genericService.load(model);
		if (wfMessage == null) return;//FIXME:give message
		
		WFInstance instance = wfMessage.getInstance();
		instance = (WFInstance) HibernateUtil.unproxy(instance);
		if (instance == null) return;//FIXME:give message
		
		WorkflowAware workflowAware = getSourceEntity(instance);
		
		if (workflowAware == null) return ; //FIXME:give message
		
		if (wfMessage.getHidden() != null && wfMessage.getHidden()){
			throw new ApplicationException("errors.workflow.entity.dealed", MVEL.getProperty(workflowAware.getSrcEntityKey(), workflowAware));
		}
		
		wfMessage.setStatus(decide);
		wfMessage.setHidden(true);
		wfMessage.setDescription(approveRemark);
		genericService.merge(wfMessage);
		
		WFInstanceTracker myTracker = getMyInstanceTracker(instance);
		if (myTracker != null){
			myTracker.setRemark(approveRemark);
			myTracker.setStatus(decide);
			myTracker.setModifyOn(new Date());
			genericService.merge(myTracker);
		}
		
		WFTemplate currentTemplate = (WFTemplate) HibernateUtil.unproxy(instance.getWfTemplate());
		WFPhase currentPhase = (WFPhase) HibernateUtil.unproxy(instance.getWfPhase());
		if (decide.isAccept()){
			int needUsers = currentPhase.getNeedAcceptUsers() == null ? currentPhase.getUsers().size() : currentPhase.getNeedAcceptUsers();
			if (needUsers == 0){
				checkCondition(currentPhase.getWfCondition(),  workflowAware, action);
				List<String> userCodes = (List<String>) action.get(WF_USEREX);
				if (userCodes != null ){
					needUsers = userCodes.size();
				}
			} 

			int accpetUsers = instance.getAcceptedUsers();
			if (needUsers <= accpetUsers){
				if (currentTemplate.isTheLastPhase(currentPhase)){
					instance.setStatus(decide);
					
					accept(workflowAware);
				    
				} else {
					//end this phase ,start next phase
					WFPhase nextPhase = getTemplateNextPhase(currentTemplate, workflowAware, currentPhase, action);
					if (nextPhase == null){
						instance.setStatus(decide);
						accept(workflowAware);
					} else {
						action.put(WF_USEREX, null);
						generateInstance(workflowAware, currentTemplate, nextPhase, approveRemark, action);
					}
				}
				// set orthers message.hidden=true?
				setOthersMessageHidden(workflowAware, currentTemplate, currentPhase);
			}
		} else if (decide.isReject()){
			reject(workflowAware);
			setOthersMessageHidden(workflowAware, currentTemplate, currentPhase);
		}
	}
	
	private void setOthersMessageHidden(WorkflowAware workflowAware, WFTemplate currentTemplate, WFPhase currentPhase){
		List<WFMessage> results = wfMessageService.getUnDealedWFMessagesByWorklowAwareAndPhase(workflowAware, currentTemplate, currentPhase);
		if (results != null){
			for(WFMessage message : results){
				message.setHidden(true);
				genericService.merge(message);
			}
		}
	}
	
	private void start(WorkflowAware workflowAware){
		workflowAware.onWFStarted();
		WorkflowServiceAware workflowServiceAware = getWorkflowServiceAware(workflowAware);
	    if (workflowServiceAware != null){
	    	workflowServiceAware.onWFStarted(workflowAware);
	    }
	}
	private void accept(WorkflowAware workflowAware){
		workflowAware.onWFAccept();
	    WorkflowServiceAware workflowServiceAware = getWorkflowServiceAware(workflowAware);
	    if (workflowServiceAware != null){
	    	workflowServiceAware.onWFAccept(workflowAware);
	    }
	}
	
	private void messaged(WorkflowAware workflowAware ,WFMessage message){
		WorkflowServiceAware workflowServiceAware = getWorkflowServiceAware(workflowAware);
	    if (workflowServiceAware != null){
	    	workflowServiceAware.onWFMessaged(workflowAware, message);
	    }
	}
	
	private void reject(WorkflowAware workflowAware){
		workflowAware.onWFReject();
	    WorkflowServiceAware workflowServiceAware = getWorkflowServiceAware(workflowAware);
	    if (workflowServiceAware != null){
	    	workflowServiceAware.onWFReject(workflowAware);
	    }
	}
	
	private WorkflowServiceAware getWorkflowServiceAware(WorkflowAware event){
		String serviceName = beanIntrospector.getServiceName(beanIntrospector.getSimpleName(event.getClass().getName()));
		return (WorkflowServiceAware) objectFactory.getBean(serviceName,false );
	}
	
	private Message generateWFMessage(WFInstance instance, LoginUser to){
		LoginUser currentUser = loginUserService.getLoginUser();
		WFMessage message = new WFMessage();
		WorkflowAware workflowAware = getSourceEntity(instance);
		String title = i18nService.getI18n("commons.wf.waitingAccept").getName() + ":"
				+ modelService.getI18nValue(workflowAware.getClass());
		title += "(" + MVEL.getProperty(workflowAware.getSrcEntityKey(), workflowAware) + ") "  ;
		message.setTitle(title);
		message.setInstance(instance);
		message.setFrom(currentUser);
		message.setTo(to);
		message.setHidden(false);
//		message.setDescription(instance.getDescription());
		
		
		Executor<Object, Object[]> noticeExecutor = notice.get();
		if (noticeExecutor != null) {
			noticeExecutor.execute(new Object[] {workflowAware, to});
		}
		
		message = genericService.add(message);
		
		//trigger message notice
		messaged(workflowAware, message);
		
		return message;
	}
	
	public List<Map<String,String>> getTriggerItems(){
		List<Map<String,String>> triggerItems = new ArrayList<Map<String,String>>();
		Map<String ,String> types =  new HashMap<String , String>();
		types =  new HashMap<String , String>();
		types.put("id", WFCondition.ACCEPT_TRIGGER_ALWAYS.toString());
		types.put("name", new I18n("trigger.always" ,"Always" ,"总是").getName());
		triggerItems.add(types);
		
		types =  new HashMap<String , String>();
		types.put("id", WFCondition.ACCEPT_TRIGGER_CONDITION.toString());
		types.put("name", new I18n("trigger.condition" ,"Trigger when True " ,"当满足条件时").getName());
		triggerItems.add(types);

		return triggerItems;
	}
	
	public void startProcessOrTask(String actionName, Map action) {
		startAutoNode(actionName, action);
	}
	
	public void updateAutoNodeCache(WFAutoNode node) {
		List<NodeExecutorInfo> nexts = new ArrayList<NodeExecutorInfo>();
		if (node.getNextBizNodes().size() > 0 && node.getEnable() != null && node.getEnable()) {
			for (BizNodeInfo next : node.getNextBizNodes()) {
				if (next.getModelSimpleName() == null) {
					logger.error("Skip the wrong model definition: null");
					continue;
				}
				
				String path = Routing.invoker2PathNoExtension(
						next.getModelSimpleName(), '.', next.getMethod(), ContextHolder.get().getSiteName());
				Invoker invoker = routing.path2Invoker(path, true);
				ServiceInfo serviceInfo = invocationUtil.getServiceInfo(invoker, true, true);
				if (serviceInfo == null) {
					if (logger.isEnabledFor(Level.ERROR)) {
						logger.error("Skip the wrong service definition: " + next.getModelSimpleName() + '.' + next.getMethod());
					}
				} else {
					String condition = StringUtils.isEmpty(next.getPreCondition()) ? null : next.getPreCondition();
					nexts.add(new NodeExecutorInfo(serviceInfo, condition));
				}
			}
			autoNodeCache.put(node.getAction(), nexts);
		} else {
			autoNodeCache.remove(node.getAction());
		}
	}
	
	private void startAutoNode(String actionName, Map action) {
		Map<String, List<NodeExecutorInfo>> nodeCache = autoNodeCache;
		if (nodeCache == null) {
			synchronized (this) {
				if (autoNodeCache == null) {
					Map cache = cacheService.newCache(List.class);
					autoNodeCache = cache;
					nodeCache = autoNodeCache;
					
					WFAutoNode autoNode = new WFAutoNode();
					autoNode.setEnable(Boolean.TRUE);
					List<WFAutoNode> nodes = genericService.searchAll(autoNode).getData();
					for (WFAutoNode node : nodes) {
						updateAutoNodeCache(node);
					}
					
					genericService.on(BizNodeInfo.class, "merge,delete", new Executor() {
						@Override
						public Object execute(Object parameters) {
							autoNodeCache = null;
							return null;
						}
					}, false);
					
					genericService.on(WFAutoNode.class, "merge,delete", new Executor() {
						@Override
						public Object execute(Object parameter) {
							Object[] parameters = (Object[])parameter;
							WFAutoNode newDbModel = (WFAutoNode) parameters[0];
							WFAutoNode dbModel = (WFAutoNode) parameters[1];
							String operation = (String)parameters[2];
							// FIXME: use old db action
							if (autoNodeCache != null && dbModel != null) {
								autoNodeCache.remove(dbModel.getAction());
								if (!"delete".equals(operation)) {
									updateAutoNodeCache(newDbModel);
								}
							}
							return null;
						}
					}, false);
				}
			}
		}
		
		if (nodeCache == null) return ;
		
		List<NodeExecutorInfo> nextActions = nodeCache.get(actionName);
		if (nextActions != null) {
			// share the same parameter context
			Map<String, Object> serviceParameters = new HashMap<String, Object>(action);
			serviceParameters.put("fn", functions);
			
			boolean isFirst = true;
			for (NodeExecutorInfo nodeExecutorInfo : nextActions) {
				boolean pass = true;
				
				// load the default parameters
				ServiceInfo serviceInfo = nodeExecutorInfo.serviceInfo;
				String simpleName = beanIntrospector.getSimpleName(serviceInfo.serviceType.getName());
				String invocationActionName = beanIntrospector.getActionName(simpleName, serviceInfo.serviceMethod.getName());
				Map<String, Object> parameters = invocationUtil.getDefaultParameters(serviceInfo, invocationActionName, serviceParameters);
				if (isFirst) {
					for (Entry<String, Object> parameter : parameters.entrySet()) {
						if (!serviceParameters.containsKey(parameter.getKey())) {		// user input has the higher priority
							serviceParameters.put(parameter.getKey(), parameter.getValue());
						}
					}
				} else {
					serviceParameters.putAll(parameters);
				}
				
				// execute condition
				if (nodeExecutorInfo.condition != null && nodeExecutorInfo.condition.length() > 0) {
					// create variable: dbmodel, dbresults
					invocationUtil.forceLoadModelAndResults(serviceParameters);
					
					Map<String, Object> traceResult = new HashMap<String, Object>();
					Collection<String> parameterNames = nodeExecutorInfo.serviceInfo.parameteTypes.keySet();
					// all the method parameters
					for (String parameterName : parameterNames) {
						traceResult.put(parameterName, null);
					}
					// plus result parameter
					traceResult.put("result", null);
					
					String result = FreemarkerTplEngine.getInstance().execute(nodeExecutorInfo.condition, serviceParameters, traceResult);
					String noBlankResult = StringUtils.trim(result);
					Object resultVar = traceResult.get("result");
					if (resultVar != null) {
						pass = Boolean.TRUE.equals(resultVar) || "true".equals(resultVar);
					} else {
						pass = "true".equals(noBlankResult) || "".equals(noBlankResult);
					}
					
					if (pass) {
						serviceParameters.putAll(traceResult);
					}
				}
				
				if (pass) {
					try {
						invocationUtil.invoke(nodeExecutorInfo.serviceInfo, serviceParameters, false);
					} catch (Exception e) {
						if (logger.isEnabledFor(Level.ERROR)) {
							logger.error("Failed to execute :" + nodeExecutorInfo.serviceInfo.serviceMethod.getName());
						}
					}
				}
			}
		}
	}
}